4.6 错误处理
返古的错误处理方式,是Go被谈及最多的内容之一。有人戏称做“Stuck in 70’s”,可见它与流行趋势背道而驰。
error
官方推荐的标准做法是返回error状态。
func Scanln(a...interface{}) (n int,err error)标准库将error定义为接口类型,以便实现自定义错误类型。
type error interface{
Error()string
}按惯例,error总是最后一个返回参数。标准库提供了相关创建函数,可方便地创建包含简单错误文本的error对象。
var errDivByZero=errors.New("division by zero")
func div(x,y int) (int,error) {
if y==0{
return 0,errDivByZero
}
return x/y,nil
}
func main() {
z,err:=div(5,0)
if err==errDivByZero{
log.Fatalln(err)
}
println(z)
}应通过错误变量,而非文本内容来判定错误类别。
错误变量通常以err作为前缀,且字符串内容全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出。
全局错误变量并非没有问题,因为它们可被用户重新赋值,这就可能导致结果不匹配。不知道以后是否会出现只读变量功能,否则就只能依靠自觉了。
与errors.New类似的还有fmt.Errorf,它返回一个格式化内容的错误对象。
某些时候,我们需要自定义错误类型,以容纳更多上下文状态信息。这样的话,还可基于类型做出判断。
type DivError struct{ // 自定义错误类型
x,y int
}
func(DivError) Error() string{ // 实现error接口方法
return "division by zero"
}
func div(x,y int) (int,error) {
if y==0{
return 0,DivError{x,y} // 返回自定义错误类型
}
return x/y,nil
}
func main() {
z,err:=div(5,0)
if err!=nil{
switch e:=err.(type) { // 根据类型匹配
case DivError:
fmt.Println(e,e.x,e.y)
default:
fmt.Println(e)
}
log.Fatalln(err)
}
println(z)
}自定义错误类型通常以Error为名称后缀。在用switch按类型匹配时,注意case顺序。应将自定义类型放在前面,优先匹配更具体的错误类型。
在正式代码中,我们不能忽略error返回值,应严格检查,否则可能会导致错误的逻辑状态。调用多返回值函数时,除error外,其他返回值同样需要关注。
以os.File.Read方法为例,它会同时返回剩余内容和EOF。
大量函数和方法返回error,使得调用代码变得很难看,一堆堆的检查语句充斥在代码行间。解决思路有:
- 使用专门的检查函数处理错误逻辑(比如记录日志),简化检查代码。
- 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)。
- 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。
panic,recover
与error相比,panic/recover在使用方法上更接近try/catch结构化异常。
func panic(v interface{})
func recover()interface{}比较有趣的是,它们是内置函数而非语句。panic会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover可捕获并返回panic提交的错误对象。
func main() {
defer func() {
if err:=recover();err!=nil{ // 捕获错误
log.Fatalln(err)
}
}()
panic("i am dead") // 引发错误
println("exit.") // 永不会执行
}因为panic参数是空接口类型,因此可使用任何对象作为错误状态。而recover返回结果同样要做转型才能获得具体信息。
无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。
func test() {
defer println("test.1")
defer println("test.2")
panic("i am dead")
}
func main() {
defer func() {
log.Println(recover())
}()
test()
}
输出:
test.2
test.1
i am dead
连续调用panic,仅最后一个会被recover捕获。
func main() {
defer func() {
for{
if err:=recover();err!=nil{
log.Println(err)
}else{
log.Fatalln("fatal")
}
}
}()
defer func() {
panic("you are dead") // 类似重新抛出异常(rethrow)
}() // 可先recover捕获,包装后重新抛出
panic("i am dead")
}
输出:
you are dead
fatal
在延迟函数中panic,不会影响后续延迟调用执行。而recover之后panic,可被再次捕获。另外,recover必须在延迟调用函数中执行才能正常工作。
func catch() {
log.Println("catch:",recover())
}
func main() {
defer catch() // 捕获
defer log.Println(recover()) // 失败!
defer recover() // 失败!
panic("i am dead")
}输出:
<nil>
catch:i am dead
考虑到recover特性,如果要保护代码片段,那么只能将其重构为函数调用。
func test(x,y int) {
z:=0
func() { // 利用匿名函数保护 “z=x/y”
defer func() {
if recover() !=nil{
z=0
}
}()
z=x/y
}()
println("x/y=",z)
}
func main() {
test(5,0)
}调试阶段,可使用runtime/debug.PrintStack函数输出完整调用堆栈信息。
import(
"runtime/debug"
)
func test() {
panic("i am dead")
}
func main() {
defer func() {
if err:=recover();err!=nil{
debug.PrintStack()
}
}()
test()
}输出:
goroutine 1[running]:
main.main.func1()
test.go:15+0x6c
panic(0x7e3a0,0xc82000a260)
runtime/panic.go:426+0x4e9
main.test()
test.go:8+0x65
main.main()
test.go:20+0x35
建议:除非是不可恢复性、导致系统无法正常工作的错误,否则不建议使用panic。
例如:文件系统没有操作权限,服务端口被占用,数据库未启动等情况。